import getopt
import logging
import logging.config
import re
import socket
from queue import Queue
from threading import Thread, Lock, Event

from apscheduler.schedulers.background import BackgroundScheduler

from database import *
from terminal_device import *


class MyThread(Thread):
    '''
    用这个类实现的线程可以获取返回值
    '''

    def __init__(self, foo, args=None, daemon=False):
        Thread.__init__(self)
        self.foo = foo
        self.args = args
        self.result = None
        self.daemon = daemon

    def run(self):
        if self.args is not None:
            self.result = self.foo(*self.args)
        else:
            self.result = self.foo()

    def get_result(self):
        return self.result


class DataCenter(object):
    """
    这个类实现了服务端的业务逻辑
    """

    def __init__(self):

        self.login_token = b"\x12\x34"  # 4g dtu 登录密码
        self.login_ack = b"\x56\x78"  # 4g dtu 登录后的回复
        self.db_username = "miaow"  # 数据库用户
        self.db_password = "Guo88703266yiren"  # 数据库密码
        self.db_host = ("miaowfun.mysql.rds.aliyuncs.com", 10002)  # 数据库ip和端口
        self.db_dbname = "hehai_jieshuiyuan"  # 数据库的database名
        self.rtu_address_list = [0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0xfe,
                                 0x80]  # rtu1~7、dtu8的modbus地址, dtu8就是树莓派节点的modbus地址
        self.listen_address = ("", 10001)  # 服务端监听地址，用于连接4g dtu
        self.accept_timeout = 10  # TCP连接中服务端accept后等待的超时时间，单位秒
        self.alive_interval = 30  # 服务器检查和4g dtu连接状态的时间间隔，单位秒
        self.read_all_device_interval = 3600  # 读取状态到数据库的间隔时间，单位秒
        self.rtu_device_list = []  # 连接上的rtu和dtu设备对象
        self.sock_lock = Lock()
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.log_conf_file = r"./log/server_log.conf"  # 日志设置
        self.logger_name = "DataCenter"  # 日志设置
        self.need_exit = Event()  # 是否接收到退出指令
        self.is_4g_rtu_connected = Event()  # 是否已经和4g dtu连接
        self.command_queue = Queue()  # 待执行命令的队列
        self.scheduler = BackgroundScheduler()  # 调度器
        self.data_base = None
        self.zs_4g_dtu_device = None
        self.last_time = None
        self.sock = None
        self.client_addr = None
        self.check_alive_task = None
        self.exec_task = None
        self.cli_task = None
        self.db_task = None

        # 日志记录器，加载self.log_conf_file指定的配置文件
        logging.config.fileConfig(self.log_conf_file)
        self.logger = logging.getLogger(self.logger_name)
        self.data_base = Database(self.db_username, self.db_password, self.db_host, self.db_dbname)

    def start(self):
        """
        服务器逻辑入口
        这个方法启动各个线程，监听连接设备的端口
        """

        # 开启cli线程，用于接收用户命令
        self.cli_task = MyThread(self._cli, daemon=True)
        self.cli_task.start()

        # 开启db线程，用于接收前端命令
        self.db_task = MyThread(self._db, daemon=True)
        self.db_task.start()

        # 监听端口，等待设备连接
        self.s.bind(self.listen_address)
        self.s.listen(1)
        self.s.settimeout(self.accept_timeout)

        # 每固定时间（self.read_all_device_interval）读取一次所有设备的状态
        self.scheduler.start()

        # 若没有收到退出命令，则一直运行
        while not self.need_exit.is_set():
            try:
                self.logger.debug(f"listen {self.listen_address[0]}:{self.listen_address[1]} for connection")  # tag2
                try:
                    sock, client_addr = self.s.accept()  # 超过self.accept_timeout设定的时间后没有接受到连接就出错
                except Exception as e:
                    if self.need_exit.is_set():
                        break  # 如果收到退出命令，则结束整个进程
                    else:
                        raise e  # 此处raise的连接超时错误会被上面一层的try捕获，跳转到tag1
                self.logger.debug(f"Connection from {client_addr[0]}:{client_addr[1]}, verifying...")
                data = sock.recv(1024)  # 接受4g dtu传来的登陆包

                if data is None or data != self.login_token:  # 校验登录数据
                    self.logger.info(
                        f"Connection from {client_addr[0]}:{client_addr[1]} not verified, closing connection")
                    sock.close()
                    continue  # 登录数据错误则回到tag2处
                self.logger.info(f"Connection from {client_addr[0]}:{client_addr[1]} established and verified")
                self.is_4g_rtu_connected.set()  # 设置标志，表示4g dtu已连接
                self.client_addr = client_addr
                self.sock = sock
                self.sock.send(self.login_ack)  # 发送登录确认包
                time.sleep(2)
                self.zs_4g_dtu_device = Zs4gDtuDevice(self.sock, self.client_addr)  # 创建众山4gdtu对象

                # self.rtu_device_list保存了6个节点模块的地址，最后一个是连接继电器模块（ZzRtuDevice，命名zz来自于ZZ-IO1600，命名为rtu表示他有继电器）的dtu，其余依次为rtu1、rtu2到rtu6
                # 用self.rtu_address_list存储的地址初始化ZsDtuRtuDevice和ZzRtuDevice
                self.rtu_device_list.clear()
                for addr in self.rtu_address_list[:6]:
                    self.rtu_device_list.append(ZsDtuRtuDevice(self.sock, addr))
                self.rtu_device_list.append(ZzRtuDevice(self.sock, self.rtu_address_list[6]))
                self.rtu_device_list.append(RaspDevice(self.sock, self.rtu_address_list[7]))

                # 开始执行队列里的命令
                self.exec_task = MyThread(self._exec, daemon=True)
                self.exec_task.start()

                # 每30min读取一次所有设备的状态
                self.scheduler.add_job(self._read_status_every_30min, "interval", seconds=self.read_all_device_interval,
                                       id="read_status_every_30min")

                # 检查设备连接，若连接断开，则check_alive_task线程结束
                self.check_alive_task = MyThread(self._check_alive, daemon=True)
                self.check_alive_task.start()
                self.check_alive_task.join()  # 正常运行过程中这个线程阻塞在这里

                self.scheduler.remove_job("read_status_every_30min")  # tag6: 收到退出指令或设备掉线
                self.sock.close()
            except Exception as e:
                self.logger.warning(f"Error when login: {e}")  # tag1: 主要用于捕获连接超时错误，然后tag2处进行下一次连接

        self.close()
        self.logger.info("Server stopped")

    def _exec(self):
        """
        这个线程用于解析和执行指令
        """
        self.logger.debug("Exec thread start")

        while not self.need_exit.is_set() and self.is_4g_rtu_connected.is_set():  # 收到退出指令或设备掉线直接结束线程
            time.sleep(0.5)  # 最快每0.5s执行一个队列里的指令
            try:
                if self.command_queue.empty():
                    continue
                command = self.command_queue.get().split()
                self.command_queue.task_done()
                opts, args = getopt.getopt(command[1:], "d:i:s:c:I:")  # 解析-d -i -s- c -I参数
                device, interface, state, channel = None, None, None, None
                device_index, interface_index, database_id = None, None, None
                for o, a in opts:
                    a_lower = str.lower(a)
                    if o in ("-d", "--device") and re.search(r"^[rd]tu\d$", a_lower) is not None:
                        device = a
                        device_index = int(
                            device[3:]) - 1  # 命令里rtu从"rtu1"开始，程序里device_index从0开始，用于缩影self.rtu_device_list列表
                    elif o in ("-i", "--interface") and re.search(r"^d[io]\d{1,2}$", a_lower) is not None:
                        interface = a
                        interface_index = int(interface[2:]) - 1
                    elif o in ("-s", "--state") and a_lower in ("on", "off"):
                        state = a_lower
                    elif o in ("-c", "--channel"):
                        channel = a_lower  # "cli"表示命令来自用户命令行输入，"web"表示命令来自网页操作
                    elif o in ("-I", "--id"):
                        database_id = int(a_lower)  # 命令来自web，则记录指令状态表中的相应id值，以便tag3处通知web端执行状态
                    else:
                        raise AssertionError(f"Unsupported option {o} {a_lower}")

                if command[0] == "set":  # 命令是写DO
                    if device is None or interface is None or state is None or interface[1] == "i":
                        raise AssertionError("Unsupported command")
                    self.logger.debug("Setting DO...")
                    with self.sock_lock:  # 执行指令
                        self.rtu_device_list[device_index].set_do(interface_index,
                                                                  ModbusWriteSingleCoilValueEnum.ON if state == "on" else ModbusWriteSingleCoilValueEnum.OFF)

                    # 判断要记录到哪个数据表
                    if device_index == 6:  # 继电器模块用insert_relay_history方法
                        if channel is not None and channel == "web":  # tag3: 来自网页的命令需要和网页显示端交互，将指令状态表中对应行设为执行成功状态
                            self.data_base.update_relay_command(database_id, DatabaseCommandState.SUCCESS)
                            self.logger.debug("Write database 'relay_command' status: SUCCESS")
                        self.logger.debug(
                            f"Insert database 'relay_command': set RTU{device_index + 1}(1 based) DO{interface_index + 1}(1 based)={state}")
                        self.data_base.insert_relay_history(do_idx_state=(interface_index, 1 if state == "on" else 0))
                    else:  # rtu模块用insert_rtu_history方法
                        if channel is not None and channel == "web":  # tag3: 来自网页的命令需要和网页显示端交互，将指令状态表中对应行设为执行成功状态
                            self.data_base.update_rtu_command(database_id, DatabaseCommandState.SUCCESS)
                            self.logger.debug("Write database 'rtu_command' status: SUCCESS")
                        self.logger.debug(
                            f"Insert database 'rtu_command': set RTU{device_index + 1}(1 based) DO{interface_index + 1}(1 based)={state}")
                        self.data_base.insert_rtu_history(rtu_idx=device_index,
                                                          do_idx_state=(interface_index, 1 if state == "on" else 0))
                    self.logger.debug("Set DO done")
                elif command[0] == "get":  # 命令是获取状态
                    if device is None:
                        raise AssertionError("Unsupported command")
                    self.logger.debug("Getting DO/WATER_LEVEL...")
                    with self.sock_lock:
                        if hasattr(self.rtu_device_list[device_index], "get_dos"):
                            result_o = self.rtu_device_list[device_index].get_dos()
                        elif hasattr(self.rtu_device_list[device_index], "get_regs"):
                            result_o = self.rtu_device_list[device_index].get_regs()

                    if device_index == 6:  # 继电器模块
                        self.logger.debug(
                            f"DO State: {result_o[0]}, {result_o[1]}, {result_o[2]}, {result_o[3]}, {result_o[4]}, {result_o[5]}, "
                            f"{result_o[6]}, {result_o[7]}, {result_o[8]}, {result_o[9]}, {result_o[10]}, {result_o[11]}")
                        self.data_base.insert_relay_history(result_o)  # 继电器模块各端口状态写入数据库
                        self.logger.debug("Write database 'relay_history'")
                        if channel is not None and channel == "web":
                            self.data_base.update_relay_command(database_id,
                                                                DatabaseCommandState.SUCCESS)  # tag3: 来自网页的命令需要和网页显示端交互，将指令状态表中对应行设为执行成功状态
                            self.logger.debug("Write database 'relay_command' status: SUCCESS")
                    elif 0 <= device_index < 6:  # rtu模块
                        with self.sock_lock:
                            result_i = self.rtu_device_list[device_index].get_dis()
                        self.logger.debug(f"DO State: {result_o[0]}, {result_o[1]}")
                        self.logger.debug(f"DI State: {result_i[0]}, {result_i[1]}")
                        self.data_base.insert_rtu_history(device_index, (result_o, result_i))  # rtu各端口状态写入数据库
                        self.logger.debug("Write database 'rtu_history'")
                        if channel is not None and channel == "web":
                            self.data_base.update_rtu_command(database_id,
                                                              DatabaseCommandState.SUCCESS)  # tag3: 来自网页的命令需要和网页显示端交互，将指令状态表中对应行设为执行成功状态
                            self.logger.debug("Write database 'rtu_command' status: SUCCESS")
                    else:  # 树莓派
                        self.logger.debug(f"Water Level: {result_o[0] / 100}cm")
                        self.data_base.insert_rasp_history(device_index, result_o)  # 继电器模块各端口状态写入数据库
                        self.logger.debug("Write database 'rasp_history'")
                        if channel is not None and channel == "web":
                            self.data_base.update_rasp_command(database_id,
                                                                      DatabaseCommandState.SUCCESS)  # tag3: 来自网页的命令需要和网页显示端交互，将指令状态表中对应行设为执行成功状态
                            self.logger.debug("Write database 'rasp_history' status: SUCCESS")
                else:  # 命令既不是读也不是写
                    raise AssertionError(f"Unsupported command {command[0]}")
            except getopt.GetoptError as e:
                self.logger.warning(e.__str__())
            except ConnectionError as e:
                self.logger.warning(f"Connection error: {e}")
            except mysql.connector.Error as e:
                self.logger.warning(f"Database error: {e}")
            except Exception as e:
                self.logger.warning(f"Error: {e}")

        self.logger.debug("Exec thread exit")

    def _cli(self):
        """
        命令行界面，用于接受用户直接输入的命令
        具体指令见运维说明.pdf的后台服务指令章节
        输入命令中不用带上"-c cli"，tag4处会处理
        """
        self.logger.debug("cli thread Start")
        while True:
            self.command_queue.join()
            tmp = input("\r\n>> ").strip()
            if tmp == "" or tmp.isspace() is True:
                continue
            if tmp == "exit" or tmp == "quit" or tmp == "q":
                self.need_exit.set()  # 收到退出命令，设置标志，等待各线程结束
                while self.command_queue.unfinished_tasks > 0:
                    self.command_queue.task_done()  # 丢弃未执行的命令
                break
            tmp = tmp + " -c cli"  # tag4
            self.command_queue.put(tmp)
        self.logger.debug("Cli thread exit, server is shutdowning")

    def _check_alive(self):
        """
        每隔设定时间检查一次设备在线情况
        """
        self.logger.debug("Check alive thread start")
        while not self.need_exit.is_set():
            try:
                with self.sock_lock:
                    is_alive = self.zs_4g_dtu_device.is_alive()
                if not is_alive:
                    raise ConnectionAbortedError("4g dtu no response")  # 设备掉线则到tag5处
                else:
                    time.sleep(self.alive_interval)
            except Exception as e:  # tag5:退出循环
                self.logger.warning(f"Loss connection with {self.client_addr[0]}:{self.client_addr[1]}, exception={e}")
                break
        self.is_4g_rtu_connected.clear()  # 清除设备已连接标志
        self.sock.close()  # 关闭连接，最终退出这个线程时会到tag6处
        self.logger.debug("Check alive thread exit")

    def _db(self):
        """
        读取数据库中来自web的指令的线程
        """
        self.is_4g_rtu_connected.wait()  # 等待设备已连接
        self.logger.debug("Database thread start")
        while not self.need_exit.is_set():

            # 等待队列清空再继续，防止在数据库里反复查询到同一条未标记为执行成功的指令
            self.command_queue.join()
            time.sleep(2)
            try:
                # 读取数据库中等待执行的指令
                cmd_rtu, device_idx_rtu, do_idx_rtu, state_rtu, id_rtu = self.data_base.query_rtu_command()
                cmd_relay, device_idx_relay, do_idx_relay, state_relay, id_relay = self.data_base.query_relay_command()
                cmd_rasp, device_idx_dtu, reg_idx_rtu, state_rasp, id_rasp = self.data_base.query_rasp_command()

                # 把指令加入到指令队列
                if cmd_rtu is not None:
                    if cmd_rtu == DatabaseCommandState.SET:
                        self.logger.info(
                            f"Add to queue: set -d rtu{device_idx_rtu + 1} -i do{do_idx_rtu + 1} -s {state_rtu} -c web -I {id_rtu}")
                        self.command_queue.put(
                            f"set -d rtu{device_idx_rtu + 1} -i do{do_idx_rtu + 1} -s {state_rtu} -c web -I {id_rtu}")
                    elif cmd_rtu == DatabaseCommandState.GET:
                        self.logger.info(f"Add to queue: get -d rtu{device_idx_rtu + 1} -c web -I {id_rtu}")
                        self.command_queue.put(f"get -d rtu{device_idx_rtu + 1} -c web -I {id_rtu}")
                if cmd_relay is not None:
                    if cmd_relay == DatabaseCommandState.SET:
                        self.logger.info(
                            f"Add to queue: set -d rtu7 -i do{do_idx_relay + 1} -s {state_relay} -c web -I {id_relay}")
                        self.command_queue.put(
                            f"set -d rtu7 -i do{do_idx_relay + 1} -s {state_relay} -c web -I {id_relay}")
                    elif cmd_relay == DatabaseCommandState.GET:
                        self.logger.info(f"Add to queue: get -d rtu7 -c web -I {id_relay}")
                        self.command_queue.put(f"get -d rtu7 -c web -I {id_relay}")
                if cmd_rasp is not None:
                    if cmd_rasp == DatabaseCommandState.GET:
                        self.logger.info(f"Add to queue: get -d dtu{device_idx_dtu + 1} -c web -I {id_rasp}")
                        self.command_queue.put(f"get -d dtu{device_idx_dtu + 1} -c web -I {id_rasp}")
                    else:
                        raise AssertionError("Water level can only be read.")
            except Exception as e:
                self.logger.warning("Mysql error", e)
        self.logger.debug("Database thread exit")

    def _read_status_every_30min(self):
        """
        定时读取所有设备的状态
        虽然函数名暗示是30分钟，但间隔时间最终由self.read_all_device_interval指定
        """
        self.logger.debug(f"Reading status every {self.read_all_device_interval / 60.0}min...")
        # 把指令加入到指令队列
        for i in ["get -d rtu1", "get -d rtu2", "get -d rtu3", "get -d rtu4", "get -d rtu5", "get -d rtu6",
                  "get -d rtu7", "get -d dtu8"]:
            self.command_queue.put(i + " -c sch")

    def close(self):
        self.is_4g_rtu_connected.clear()
        if self.s is not None:
            self.s.close()
        if self.sock is not None:
            self.sock.close()


if __name__ == '__main__':
    import faulthandler

    faulthandler.enable()
    d = DataCenter()
    d.start()
